require 'tmpdir'
require 'terminal-table'
require 'emoji_regex'

require 'fastlane_core/itunes_transporter'
require 'fastlane_core/build_watcher'
require 'fastlane_core/ipa_upload_package_builder'
require_relative 'manager'

module Pilot
  class BuildManager < Manager
    def upload(options)
      # Only need to login before upload if no apple_id was given
      # 'login' will be deferred until before waiting for build processing
      should_login_in_start = options[:apple_id].nil?
      start(options, should_login: should_login_in_start)

      options[:changelog] = self.class.sanitize_changelog(options[:changelog]) if options[:changelog]

      UI.user_error!("No ipa file given") unless config[:ipa]

      if options[:changelog].nil? && options[:distribute_external] == true
        if UI.interactive?
          options[:changelog] = UI.input("No changelog provided for new build. You can provide a changelog using the `changelog` option. For now, please provide a changelog here:")
        else
          UI.user_error!("No changelog provided for new build. Please either disable `distribute_external` or provide a changelog using the `changelog` option")
        end
      end

      UI.success("Ready to upload new build to TestFlight (App: #{fetch_apple_id})...")

      dir = Dir.mktmpdir

      platform = fetch_app_platform
      package_path = FastlaneCore::IpaUploadPackageBuilder.new.generate(app_id: fetch_apple_id,
                                                                      ipa_path: options[:ipa],
                                                                  package_path: dir,
                                                                      platform: platform)

      transporter = transporter_for_selected_team(options)
      result = transporter.upload(fetch_apple_id, package_path)

      unless result
        UI.user_error!("Error uploading ipa file, for more information see above")
      end

      UI.success("Successfully uploaded the new binary to App Store Connect")

      if config[:skip_waiting_for_build_processing]
        UI.important("Skip waiting for build processing")
        UI.important("This means that no changelog will be set and no build will be distributed to testers")
        return
      end

      # Calling login again here is needed if login was not called during 'start'
      login unless should_login_in_start

      UI.message("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option")
      latest_build = wait_for_build_processing_to_be_complete
      distribute(options, build: latest_build)
    end

    def wait_for_build_processing_to_be_complete
      platform = fetch_app_platform
      app_version = FastlaneCore::IpaFileAnalyser.fetch_app_version(config[:ipa])
      app_build = FastlaneCore::IpaFileAnalyser.fetch_app_build(config[:ipa])
      latest_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: app.apple_id, platform: platform, train_version: app_version, build_version: app_build, poll_interval: config[:wait_processing_interval], strict_build_watch: config[:wait_for_uploaded_build])

      unless latest_build.train_version == app_version && latest_build.build_version == app_build
        UI.important("Uploaded app #{app_version} - #{app_build}, but received build #{latest_build.train_version} - #{latest_build.build_version}.")
      end

      return latest_build
    end

    def distribute(options, build: nil)
      start(options)
      if config[:apple_id].to_s.length == 0 && config[:app_identifier].to_s.length == 0
        config[:app_identifier] = UI.input("App Identifier: ")
      end

      build ||= Spaceship::TestFlight::Build.latest(app_id: app.apple_id, platform: fetch_app_platform)
      if build.nil?
        UI.user_error!("No build to distribute!")
      end

      # Update beta app meta info
      # 1. Demo account required
      # 2. App info
      # 3. Localized app  info
      # 4. Localized build info
      # 5. Auto notify enabled with config[:notify_external_testers]
      update_beta_app_meta(options, build)

      return if config[:skip_submission]
      if options[:reject_build_waiting_for_review]
        waiting_for_review_build = Spaceship::TestFlight::Build.all_waiting_for_review(app_id: build.app_id, platform: fetch_app_platform).first
        unless waiting_for_review_build.nil?
          UI.important("Another build is already in review. Going to expire that build and submit the new one.")
          UI.important("Expiring build: #{waiting_for_review_build.train_version} - #{waiting_for_review_build.build_version}")
          waiting_for_review_build.expire!
          UI.success("Expired previous build: #{waiting_for_review_build.train_version} - #{waiting_for_review_build.build_version}")
        end
      end
      distribute_build(build, options)
      type = options[:distribute_external] ? 'External' : 'Internal'
      UI.success("Successfully distributed build to #{type} testers 🚀")
    end

    def list(options)
      start(options)
      if config[:apple_id].to_s.length == 0 && config[:app_identifier].to_s.length == 0
        config[:app_identifier] = UI.input("App Identifier: ")
      end

      platform = fetch_app_platform(required: false)
      builds = app.all_processing_builds(platform: platform) + app.builds(platform: platform)
      # sort by upload_date
      builds.sort! { |a, b| a.upload_date <=> b.upload_date }
      rows = builds.collect { |build| describe_build(build) }

      puts(Terminal::Table.new(
             title: "#{app.name} Builds".green,
             headings: ["Version #", "Build #", "Installs"],
             rows: FastlaneCore::PrintTable.transform_output(rows)
      ))
    end

    def update_beta_app_meta(options, build)
      # App Store Connect API build id
      build_id = build.find_app_store_connect_build["id"]

      # Setting account required wth AppStore Connect API
      update_review_detail(build.app_id, { demo_account_required: options[:demo_account_required] })

      if should_update_beta_app_review_info(options)
        update_review_detail(build.app_id, options[:beta_app_review_info])
      end

      if should_update_localized_app_information?(options)
        update_localized_app_review(build.app_id, options[:localized_app_info])
      elsif should_update_app_test_information?(options)
        default_info = {}
        default_info[:feedback_email] = options[:beta_app_feedback_email] if options[:beta_app_feedback_email]
        default_info[:description] = options[:beta_app_description] if options[:beta_app_description]
        begin
          update_localized_app_review(build.app_id, {}, default_info: default_info)
          UI.success("Successfully set the beta_app_feedback_email and/or beta_app_description")
        rescue => ex
          UI.user_error!("Could not set beta_app_feedback_email and/or beta_app_description: #{ex}")
        end
      end

      if should_update_localized_build_information?(options)
        update_localized_build_review(build_id, options[:localized_build_info])
      elsif should_update_build_information?(options)
        begin
          update_localized_build_review(build_id, {}, default_info: { whats_new: options[:changelog] })
          UI.success("Successfully set the changelog for build")
        rescue => ex
          UI.user_error!("Could not set changelog: #{ex}")
        end
      end

      update_build_beta_details(build_id, {
        auto_notify_enabled: options[:notify_external_testers]
      })
    end

    def self.truncate_changelog(changelog)
      max_changelog_length = 4000
      if changelog && changelog.length > max_changelog_length
        original_length = changelog.length
        bottom_message = "..."
        changelog = "#{changelog[0...max_changelog_length - bottom_message.length]}#{bottom_message}"
        UI.important("Changelog has been truncated since it exceeds Apple's #{max_changelog_length} character limit. It currently contains #{original_length} characters.")
      end
      changelog
    end

    def self.strip_emoji(changelog)
      if changelog && changelog =~ EmojiRegex::Regex
        changelog.gsub!(EmojiRegex::Regex, "")
        UI.important("Emoji symbols have been removed from the changelog, since they're not allowed by Apple.")
      end
      changelog
    end

    def self.sanitize_changelog(changelog)
      changelog = strip_emoji(changelog)
      truncate_changelog(changelog)
    end

    private

    def describe_build(build)
      row = [build.train_version,
             build.build_version,
             build.install_count]

      return row
    end

    def should_update_beta_app_review_info(options)
      !options[:beta_app_review_info].nil?
    end

    def should_update_build_information?(options)
      options[:changelog].to_s.length > 0
    end

    def should_update_app_test_information?(options)
      options[:beta_app_description].to_s.length > 0 || options[:beta_app_feedback_email].to_s.length > 0
    end

    def should_update_localized_app_information?(options)
      !options[:localized_app_info].nil?
    end

    def should_update_localized_build_information?(options)
      !options[:localized_build_info].nil?
    end

    # If itc_provider was explicitly specified, use it.
    # If there are multiple teams, infer the provider from the selected team name.
    # If there are fewer than two teams, don't infer the provider.
    def transporter_for_selected_team(options)
      generic_transporter = FastlaneCore::ItunesTransporter.new(options[:username], nil, false, options[:itc_provider])
      return generic_transporter if options[:itc_provider] || Spaceship::Tunes.client.nil?
      return generic_transporter unless Spaceship::Tunes.client.teams.count > 1

      begin
        team = Spaceship::Tunes.client.teams.find { |t| t['contentProvider']['contentProviderId'].to_s == Spaceship::Tunes.client.team_id }
        name = team['contentProvider']['name']
        provider_id = generic_transporter.provider_ids[name]
        UI.verbose("Inferred provider id #{provider_id} for team #{name}.")
        return FastlaneCore::ItunesTransporter.new(options[:username], nil, false, provider_id)
      rescue => ex
        UI.verbose("Couldn't infer a provider short name for team with id #{Spaceship::Tunes.client.team_id} automatically: #{ex}. Proceeding without provider short name.")
        return generic_transporter
      end
    end

    def distribute_build(uploaded_build, options)
      UI.message("Distributing new build to testers: #{uploaded_build.train_version} - #{uploaded_build.build_version}")

      # This is where we could add a check to see if encryption is required and has been updated
      set_export_compliance_if_needed(uploaded_build, options)

      if options[:groups] || options[:distribute_external]
        begin
          uploaded_build.submit_for_testflight_review!
        rescue => ex
          # App Store Connect currently may 504 on this request even though it manages to get the build in
          # the approved state, this is a temporary workaround.
          raise ex unless ex.to_s.include?("504")
          UI.message("Submitting the build for review timed out, trying to recover.")
          updated_build = Spaceship::TestFlight::Build.find(app_id: uploaded_build.app_id, build_id: uploaded_build.id)
          raise ex unless updated_build.approved?
        end
      end

      if options[:groups]
        client = Spaceship::ConnectAPI::Base.client
        beta_group_ids = client.get_beta_groups(filter: { app: uploaded_build.app_id }).select do |group|
          options[:groups].include?(group["attributes"]["name"])
        end.map do |group|
          group["id"]
        end

        unless beta_group_ids.empty?
          build = uploaded_build.find_app_store_connect_build
          build_id = build["id"]

          client.add_beta_groups_to_build(build_id: build_id, beta_group_ids: beta_group_ids)
        end
      end

      if options[:distribute_external] && options[:groups].nil?
        # Legacy Spaceship::TestFlight API used to have a `default_external_group` that would automatically
        # get selected but this no longer exists with Spaceship::ConnectAPI
        UI.user_error!("You must specify at least one group using the `:groups` option to distribute externally")
      end

      true
    end

    def set_export_compliance_if_needed(uploaded_build, options)
      build = uploaded_build.find_app_store_connect_build
      build_attributes = build["attributes"] || {}
      if build_attributes["usesNonExemptEncryption"].nil?
        uses_non_exempt_encryption = options[:uses_non_exempt_encryption]
        attributes = { usesNonExemptEncryption: uses_non_exempt_encryption }

        client = Spaceship::ConnectAPI::Base.client
        client.patch_builds(build_id: build["id"], attributes: attributes)

        UI.important("Export compliance has been set to '#{uses_non_exempt_encryption}'. Need to wait for build to finishing processing again...")
        UI.important("Set 'ITSAppUsesNonExemptEncryption' in the 'Info.plist' to skip this step and speed up the submission")
        wait_for_build_processing_to_be_complete
      end
    end

    def update_review_detail(app_id, info)
      info = info.collect { |k, v| [k.to_sym, v] }.to_h

      attributes = {}
      attributes[:contactEmail] = info[:contact_email] if info.key?(:contact_email)
      attributes[:contactFirstName] = info[:contact_first_name] if info.key?(:contact_first_name)
      attributes[:contactLastName] = info[:contact_last_name] if info.key?(:contact_last_name)
      attributes[:contactPhone] = info[:contact_phone] if info.key?(:contact_phone)
      attributes[:demoAccountName] = info[:demo_account_name] if info.key?(:demo_account_name)
      attributes[:demoAccountPassword] = info[:demo_account_password] if info.key?(:demo_account_password)
      attributes[:demoAccountRequired] = info[:demo_account_required] if info.key?(:demo_account_required)
      attributes[:notes] = info[:notes] if info.key?(:notes)

      client = Spaceship::ConnectAPI::Base.client
      client.patch_beta_app_review_detail(app_id: app_id, attributes: attributes)
    end

    def update_localized_app_review(app_id, info_by_lang, default_info: nil)
      info_by_lang = info_by_lang.collect { |k, v| [k.to_sym, v] }.to_h

      if default_info
        info_by_lang.delete(:default)
      else
        default_info = info_by_lang.delete(:default)
      end

      # Initialize hash of lang codes
      langs_localization_ids = {}

      # Validate locales exist
      client = Spaceship::ConnectAPI::Base.client
      localizations = client.get_beta_app_localizations(filter: { app: app_id })
      localizations.each do |localization|
        localization_id = localization["id"]
        attributes = localization["attributes"]
        locale = attributes["locale"]

        langs_localization_ids[locale.to_sym] = localization_id
      end

      # Create or update localized app review info
      langs_localization_ids.each do |lang_code, localization_id|
        info = info_by_lang[lang_code]

        info = default_info unless info
        update_localized_app_review_for_lang(app_id, localization_id, lang_code, info) if info
      end
    end

    def update_localized_app_review_for_lang(app_id, localization_id, locale, info)
      attributes = {}
      attributes[:feedbackEmail] = info[:feedback_email] if info.key?(:feedback_email)
      attributes[:marketingUrl] = info[:marketing_url] if info.key?(:marketing_url)
      attributes[:privacyPolicyUrl] = info[:privacy_policy_url] if info.key?(:privacy_policy_url)
      attributes[:tvOsPrivacyPolicy] = info[:tv_os_privacy_policy_url] if info.key?(:tv_os_privacy_policy_url)
      attributes[:description] = info[:description] if info.key?(:description)

      client = Spaceship::ConnectAPI::Base.client
      if localization_id
        client.patch_beta_app_localizations(localization_id: localization_id, attributes: attributes)
      else
        attributes[:locale] = locale if locale
        client.post_beta_app_localizations(app_id: app_id, attributes: attributes)
      end
    end

    def update_localized_build_review(build_id, info_by_lang, default_info: nil)
      info_by_lang = info_by_lang.collect { |k, v| [k.to_sym, v] }.to_h

      if default_info
        info_by_lang.delete(:default)
      else
        default_info = info_by_lang.delete(:default)
      end

      # Initialize hash of lang codes
      langs_localization_ids = {}

      # Validate locales exist
      client = Spaceship::ConnectAPI::Base.client
      localizations = client.get_beta_build_localizations(filter: { build: build_id })
      localizations.each do |localization|
        localization_id = localization["id"]
        attributes = localization["attributes"]
        locale = attributes["locale"]

        langs_localization_ids[locale.to_sym] = localization_id
      end

      # Create or update localized app review info
      langs_localization_ids.each do |lang_code, localization_id|
        info = info_by_lang[lang_code]

        info = default_info unless info
        update_localized_build_review_for_lang(build_id, localization_id, lang_code, info) if info
      end
    end

    def update_localized_build_review_for_lang(build_id, localization_id, locale, info)
      attributes = {}
      attributes[:whatsNew] = info[:whats_new] if info.key?(:whats_new)

      client = Spaceship::ConnectAPI::Base.client
      if localization_id
        client.patch_beta_build_localizations(localization_id: localization_id, attributes: attributes)
      else
        attributes[:locale] = locale if locale
        client.post_beta_build_localizations(build_id: build_id, attributes: attributes)
      end
    end

    def update_build_beta_details(build_id, info)
      client = Spaceship::ConnectAPI::Base.client
      resp = client.get_build_beta_details(filter: { build: build_id })
      build_beta_details_id = resp.first["id"]

      attributes = {}
      attributes[:autoNotifyEnabled] = info[:auto_notify_enabled] if info.key?(:auto_notify_enabled)

      client.patch_build_beta_details(build_beta_details_id: build_beta_details_id, attributes: attributes)
    end
  end
end
